Domina el dise帽o basado en el dominio en JavaScript. Aprende el Patr贸n de Entidad de M贸dulo para construir aplicaciones escalables, probables y mantenibles con modelos robustos de objetos de dominio.
Patrones de Entidades de M贸dulos de JavaScript: Una Inmersi贸n Profunda en el Modelado de Objetos de Dominio
En el mundo del desarrollo de software, especialmente dentro del din谩mico y en constante evoluci贸n ecosistema de JavaScript, a menudo priorizamos la velocidad, los frameworks y las funcionalidades. Construimos interfaces de usuario complejas, nos conectamos a innumerables APIs e implementamos aplicaciones a un ritmo vertiginoso. Pero en esta prisa, a veces descuidamos el n煤cleo mismo de nuestra aplicaci贸n: el dominio del negocio. Esto puede llevar a lo que a menudo se llama la "Gran Bola de Barro", un sistema donde la l贸gica de negocio est谩 dispersa, los datos no est谩n estructurados y hacer un simple cambio puede desencadenar una cascada de errores imprevistos.
Aqu铆 es donde entra en juego el Modelado de Objetos de Dominio. Es la pr谩ctica de crear un modelo rico y expresivo del espacio del problema en el que est谩s trabajando. Y en JavaScript, el Patr贸n de Entidad de M贸dulo es una forma poderosa, elegante y agn贸stica del framework para lograr esto. Esta gu铆a completa te guiar谩 a trav茅s de la teor铆a, la pr谩ctica y los beneficios de este patr贸n, permiti茅ndote construir aplicaciones m谩s robustas, escalables y mantenibles.
驴Qu茅 es el Modelado de Objetos de Dominio?
Antes de sumergirnos en el patr贸n en s铆, aclaremos nuestros t茅rminos. Es crucial distinguir este concepto del Modelo de Objetos del Documento (DOM) del navegador.
- Dominio: En software, el 'dominio' es el 谩rea tem谩tica espec铆fica a la que pertenece el negocio del usuario. Para una aplicaci贸n de comercio electr贸nico, el dominio incluye conceptos como Productos, Clientes, Pedidos y Pagos. Para una plataforma de redes sociales, incluye Usuarios, Publicaciones, Comentarios y Me Gusta.
- Modelado de Objetos de Dominio: Este es el proceso de crear un modelo de software que representa las entidades, sus comportamientos y sus relaciones dentro de ese dominio de negocio. Se trata de traducir conceptos del mundo real en c贸digo.
Un buen modelo de dominio no es solo una colecci贸n de contenedores de datos. Es una representaci贸n viva de las reglas de tu negocio. Un objeto Pedido no solo debe contener una lista de art铆culos; debe saber c贸mo calcular su total, c贸mo agregar un nuevo art铆culo y si se puede cancelar. Esta encapsulaci贸n de datos y comportamiento es la clave para construir un n煤cleo de aplicaci贸n resiliente.
El Problema Com煤n: Anarqu铆a en la Capa de "Modelo"
En muchas aplicaciones de JavaScript, especialmente aquellas que crecen org谩nicamente, la capa de 'modelo' suele ser una ocurrencia tard铆a. Frecuentemente vemos este antipatr贸n:
// En alg煤n lugar de un controlador o servicio de API...
async function createUser(req, res) {
const { email, password, firstName, lastName } = req.body;
// La l贸gica de negocio y la validaci贸n est谩n dispersas aqu铆
if (!email || !email.includes('@')) {
return res.status(400).send({ error: 'Se requiere un correo electr贸nico v谩lido.' });
}
if (!password || password.length < 8) {
return res.status(400).send({ error: 'La contrase帽a debe tener al menos 8 caracteres.' });
}
const user = {
email: email.toLowerCase(),
password: await hashPassword(password), // Alguna funci贸n de utilidad
fullName: `${firstName} ${lastName}`, // La l贸gica para los datos derivados est谩 aqu铆
createdAt: new Date()
};
// Ahora, 驴qu茅 es `user`? Es solo un objeto plano.
// Nada impide que otro desarrollador haga esto m谩s tarde:
// user.email = 'un-correo-electr贸nico-inv谩lido';
// user.password = 'corto';
await db.users.insert(user);
res.status(201).send(user);
}
Este enfoque presenta varios problemas cr铆ticos:
- Sin una 脷nica Fuente de Verdad: Las reglas para lo que constituye un 'usuario' v谩lido se definen dentro de este controlador. 驴Qu茅 pasa si otra parte del sistema necesita crear un usuario? 驴Copias y pegas la l贸gica? Esto conduce a la inconsistencia y a los errores.
- Modelo de Dominio An茅mico: El objeto `user` es solo una bolsa de datos 'tonta'. No tiene comportamiento ni autoconciencia. Toda la l贸gica que opera sobre 茅l vive externamente.
- Baja Cohesi贸n: La l贸gica para crear el nombre completo de un usuario se mezcla con el manejo de la solicitud/respuesta de la API y el hashing de la contrase帽a.
- Dif铆cil de Probar: Para probar la l贸gica de creaci贸n de usuarios, tienes que simular las solicitudes y respuestas HTTP, las bases de datos y las funciones de hashing. No puedes simplemente probar el concepto de 'usuario' de forma aislada.
- Contratos Impl铆citos: El resto de la aplicaci贸n solo tiene que 'asumir' que cualquier objeto que represente a un usuario tiene una cierta forma y que sus datos son v谩lidos. No hay garant铆as.
La Soluci贸n: El Patr贸n de Entidad de M贸dulo de JavaScript
El Patr贸n de Entidad de M贸dulo aborda estos problemas mediante el uso de un m贸dulo est谩ndar de JavaScript (un archivo) para definir todo acerca de un solo concepto de dominio. Este m贸dulo se convierte en la fuente definitiva de verdad para esa entidad.
Una Entidad de M贸dulo normalmente expone una funci贸n de f谩brica. Esta funci贸n es responsable de crear una instancia v谩lida de la entidad. El objeto que devuelve no es solo datos; es un objeto de dominio rico que encapsula sus propios datos, validaci贸n y l贸gica de negocio.
Caracter铆sticas Clave de una Entidad de M贸dulo
- Encapsulaci贸n: Agrupa los datos y las funciones que operan sobre esos datos.
- Validaci贸n en el L铆mite: Asegura que sea imposible crear una entidad inv谩lida. Protege su propio estado.
- API Clara: Expone un conjunto limpio e intencional de funciones (una API p煤blica) para interactuar con la entidad, al tiempo que oculta los detalles internos de la implementaci贸n.
- Inmutabilidad: A menudo produce objetos inmutables o de solo lectura para evitar cambios de estado accidentales y asegurar un comportamiento predecible.
- Portabilidad: No tiene dependencias de frameworks (como Express, React) o sistemas externos (como bases de datos, APIs). Es pura l贸gica de negocio.
Componentes Centrales de una Entidad de M贸dulo
Reconstruyamos nuestro concepto de `User` usando este patr贸n. Crearemos un archivo, `user.js` (o `user.ts` para los usuarios de TypeScript), y lo construiremos paso a paso.
1. La Funci贸n de F谩brica: Tu Constructor de Objetos
En lugar de clases, usaremos una funci贸n de f谩brica (por ejemplo, `buildUser`). Las f谩bricas ofrecen una gran flexibilidad, evitan luchar con la palabra clave `this` y hacen que el estado privado y la encapsulaci贸n sean m谩s naturales en JavaScript.
Nuestro objetivo es crear una funci贸n que tome datos sin procesar y devuelva un objeto Usuario bien formado y confiable.
// archivo: /domain/user.js
export default function buildMakeUser() {
// Esta funci贸n interna es la f谩brica real.
// Tiene acceso a cualquier dependencia pasada a buildMakeUser, si es necesario.
return function makeUser({
id = generateId(), // Asumamos una funci贸n para generar un ID 煤nico
firstName,
lastName,
email,
passwordHash,
createdAt = new Date()
}) {
// ... la validaci贸n y la l贸gica ir谩n aqu铆 ...
const user = {
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => email,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
};
// Usando Object.freeze para hacer el objeto inmutable.
return Object.freeze(user);
}
}
Observa algunas cosas aqu铆. Estamos usando una funci贸n que devuelve una funci贸n (una funci贸n de orden superior). Este es un patr贸n poderoso para inyectar dependencias, como un generador de ID 煤nicos o una biblioteca de validaci贸n, sin acoplar la entidad a una implementaci贸n espec铆fica. Por ahora, lo mantendremos simple.
2. Validaci贸n de Datos: El Guardi谩n en la Puerta
Una entidad debe proteger su propia integridad. Deber铆a ser imposible crear un `User` en un estado inv谩lido. Agregamos la validaci贸n justo dentro de la funci贸n de f谩brica. Si los datos no son v谩lidos, la f谩brica deber铆a lanzar un error, indicando claramente lo que est谩 mal.
// archivo: /domain/user.js
export default function buildMakeUser({ Id, isValidEmail, hashPassword }) {
return function makeUser({
id = Id.makeId(),
firstName,
lastName,
email,
password, // Ahora tomamos una contrase帽a simple y la manejamos dentro
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('El usuario debe tener un id v谩lido.');
}
if (!firstName || firstName.length < 2) {
throw new Error('El nombre debe tener al menos 2 caracteres.');
}
if (!lastName || lastName.length < 2) {
throw new Error('El apellido debe tener al menos 2 caracteres.');
}
if (!email || !isValidEmail(email)) {
throw new Error('El usuario debe tener una direcci贸n de correo electr贸nico v谩lida.');
}
if (!password || password.length < 8) {
throw new Error('La contrase帽a debe tener al menos 8 caracteres.');
}
// La normalizaci贸n y la transformaci贸n de los datos ocurren aqu铆
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
});
}
}
Ahora, cualquier parte de nuestro sistema que quiera crear un `User` debe pasar por esta f谩brica. Obtenemos una validaci贸n garantizada cada vez. Tambi茅n hemos encapsulado la l贸gica de hashing de la contrase帽a y la normalizaci贸n de la direcci贸n de correo electr贸nico. El resto de la aplicaci贸n no necesita saber ni preocuparse por estos detalles.
3. L贸gica de Negocio: Encapsulando el Comportamiento
Nuestro objeto `User` todav铆a es un poco an茅mico. Contiene datos, pero no *hace* nada. Agreguemos comportamiento: m茅todos que representen acciones espec铆ficas del dominio.
// ... dentro de la funci贸n makeUser ...
if (!password || password.length < 8) {
// ...
}
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt,
// L贸gica de Negocio / Comportamiento
getFullName: () => `${firstName} ${lastName}`,
// Un m茅todo que describe una regla de negocio
canVote: () => {
// En algunos pa铆ses, la edad para votar es de 18 a帽os. Esta es una regla de negocio.
// Asumamos que tenemos una propiedad dateOfBirth.
const age = calculateAge(dateOfBirth);
return age >= 18;
}
});
// ...
La l贸gica de `getFullName` ya no est谩 dispersa en alg煤n controlador aleatorio; pertenece a la propia entidad `User`. Cualquiera que tenga un objeto `User` ahora puede obtener de forma confiable el nombre completo llamando a `user.getFullName()`. La l贸gica se define una vez, en un solo lugar.
Construyendo un Ejemplo Pr谩ctico: Un Sistema Simple de Comercio Electr贸nico
Apliquemos este patr贸n a un dominio m谩s interconectado. Modelaremos un `Product`, un `OrderItem` y un `Order`.
1. Modelando la Entidad `Product`
Un producto tiene un nombre, un precio y alguna informaci贸n de stock. Debe tener un nombre y su precio no puede ser negativo.
// archivo: /domain/product.js
export default function buildMakeProduct({ Id }) {
return function makeProduct({
id = Id.makeId(),
name,
description,
price,
stock = 0
}) {
if (!Id.isValidId(id)) {
throw new Error('El producto debe tener un ID v谩lido.');
}
if (!name || name.trim().length < 2) {
throw new Error('El nombre del producto debe tener al menos 2 caracteres.');
}
if (isNaN(price) || price <= 0) {
throw new Error('El producto debe tener un precio mayor que cero.');
}
if (isNaN(stock) || stock < 0) {
throw new Error('El stock debe ser un n煤mero no negativo.');
}
return Object.freeze({
getId: () => id,
getName: () => name,
getDescription: () => description,
getPrice: () => price,
getStock: () => stock,
// L贸gica de negocio
isAvailable: () => stock > 0,
// Un m茅todo que modifica el estado devolviendo una nueva instancia
reduceStock: (amount) => {
if (amount > stock) {
throw new Error('No hay suficiente stock disponible.');
}
// Devuelve un NUEVO objeto product con el stock actualizado
return makeProduct({ id, name, description, price, stock: stock - amount });
}
});
}
}
Observa el m茅todo `reduceStock`. Este es un concepto crucial relacionado con la inmutabilidad. En lugar de cambiar la propiedad `stock` en el objeto existente, devuelve una *nueva* instancia de `Product` con el valor actualizado. Esto hace que los cambios de estado sean expl铆citos y predecibles.
2. Modelando la Entidad `Order` (La Ra铆z Agregada)
Un `Order` es m谩s complejo. Es lo que el Dise帽o Dirigido por el Dominio (DDD) llama una "Ra铆z Agregada". Es una entidad que administra otros objetos m谩s peque帽os dentro de su l铆mite. Un `Order` contiene una lista de `OrderItem`s. No agregas un producto directamente a un pedido; agregas un `OrderItem` que contiene un producto y una cantidad.
// archivo: /domain/order.js
export const ORDER_STATUS = {
PENDING: 'PENDING',
PAID: 'PAID',
SHIPPED: 'SHIPPED',
CANCELLED: 'CANCELLED'
};
export default function buildMakeOrder({ Id, validateOrderItem }) {
return function makeOrder({
id = Id.makeId(),
customerId,
items = [],
status = ORDER_STATUS.PENDING,
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('El pedido debe tener un ID v谩lido.');
}
if (!customerId) {
throw new Error('El pedido debe tener un ID de cliente.');
}
let orderItems = [...items]; // Crea una copia privada para administrar
return Object.freeze({
getId: () => id,
getCustomerId: () => customerId,
getItems: () => [...orderItems], // Devuelve una copia para evitar la modificaci贸n externa
getStatus: () => status,
getCreatedAt: () => createdAt,
// L贸gica de Negocio
calculateTotal: () => {
return orderItems.reduce((total, item) => {
return total + (item.getPrice() * item.getQuantity());
}, 0);
},
addItem: (item) => {
// validateOrderItem es una funci贸n que asegura que el art铆culo sea una entidad OrderItem v谩lida
validateOrderItem(item);
// Regla de negocio: evitar agregar duplicados, solo aumentar la cantidad
const existingItemIndex = orderItems.findIndex(i => i.getProductId() === item.getProductId());
if (existingItemIndex > -1) {
const newQuantity = orderItems[existingItemIndex].getQuantity() + item.getQuantity();
// Aqu铆 actualizar铆as la cantidad en el art铆culo existente
// (Esto requiere que los art铆culos sean mutables o que tengan un m茅todo de actualizaci贸n)
} else {
orderItems.push(item);
}
},
markPaid: () => {
if (status !== ORDER_STATUS.PENDING) {
throw new Error('Solo los pedidos pendientes pueden marcarse como pagados.');
}
// Devuelve una nueva instancia de Order con el estado actualizado
return makeOrder({ id, customerId, items: orderItems, status: ORDER_STATUS.PAID, createdAt });
}
});
}
}
Esta entidad `Order` ahora aplica reglas de negocio complejas:
- Administra su propia lista de art铆culos.
- Sabe c贸mo calcular su propio total.
- Aplica transiciones de estado (por ejemplo, solo puedes marcar un pedido `PENDING` como `PAID`).
La l贸gica de negocio para los pedidos ahora est谩 perfectamente encapsulada dentro de este m贸dulo, se puede probar de forma aislada y se puede reutilizar en toda la aplicaci贸n.
Patrones Avanzados y Consideraciones
Inmutabilidad: La Piedra Angular de la Predictibilidad
Hemos tocado la inmutabilidad. 驴Por qu茅 es tan importante? Cuando los objetos son inmutables, puedes pasarlos por toda tu aplicaci贸n sin temor a que alguna funci贸n distante cambie su estado inesperadamente. Esto elimina toda una clase de errores y hace que el flujo de datos de tu aplicaci贸n sea mucho m谩s f谩cil de razonar.
Object.freeze() proporciona una congelaci贸n superficial. Para las entidades con objetos o arreglos anidados (como nuestro `Order`), debes ser m谩s cuidadoso. Por ejemplo, en `order.getItems()`, devolvimos una copia (`[...orderItems]`) para evitar que la persona que llama insertara elementos directamente en el arreglo interno del pedido.
Para aplicaciones complejas, bibliotecas como Immer pueden facilitar mucho el trabajo con estructuras anidadas inmutables, pero el principio central sigue siendo: trata tus entidades como valores inmutables. Cuando sea necesario realizar un cambio, crea un nuevo valor.
Manejo de Operaciones As铆ncronas y Persistencia
Es posible que hayas notado que nuestras entidades son completamente s铆ncronas. No saben nada sobre bases de datos o APIs. 隆Esto es intencional y una gran fortaleza del patr贸n!
Las entidades no deben guardarse a s铆 mismas. El trabajo de una entidad es hacer cumplir las reglas de negocio. El trabajo de guardar datos en una base de datos pertenece a una capa diferente de tu aplicaci贸n, a menudo llamada Capa de Servicio, Capa de Caso de Uso o Patr贸n de Repositorio.
As铆 es como interact煤an:
// archivo: /use-cases/create-user.js
// Este caso de uso depende de la f谩brica de entidades de usuario y de una funci贸n de acceso a la base de datos.
export default function makeCreateUser({ makeUser, usersDatabase }) {
return async function createUser(userInfo) {
// 1. Crea una entidad de dominio v谩lida. Este paso valida los datos.
const user = makeUser(userInfo);
// 2. Verifica si hay reglas de negocio que requieran datos externos (por ejemplo, la unicidad del correo electr贸nico)
const exists = await usersDatabase.findByEmail({ email: user.getEmail() });
if (exists) {
throw new Error('La direcci贸n de correo electr贸nico ya est谩 en uso.');
}
// 3. Persiste la entidad. La base de datos necesita un objeto plano.
const persisted = await usersDatabase.insert({
id: user.getId(),
firstName: user.getFirstName(),
// ... y as铆 sucesivamente
});
return persisted;
}
}
Esta separaci贸n de responsabilidades es poderosa:
- La entidad `User` es pura, s铆ncrona y f谩cil de probar unitariamente.
- El caso de uso `createUser` es responsable de la orquestaci贸n y se puede probar la integraci贸n con una base de datos simulada.
- El m贸dulo `usersDatabase` es responsable de la tecnolog铆a de base de datos espec铆fica y se puede probar por separado.
Serializaci贸n y Deserializaci贸n
Tus entidades, con sus m茅todos, son objetos enriquecidos. Pero cuando env铆as datos a trav茅s de una red (por ejemplo, en una respuesta de API JSON) o los almacenas en una base de datos, necesitas una representaci贸n de datos simple. Este proceso se llama serializaci贸n.
Un patr贸n com煤n es agregar un m茅todo `toJSON()` o `toObject()` a tu entidad.
// ... dentro de la funci贸n makeUser ...
return Object.freeze({
getId: () => id,
// ... otros getters
// M茅todo de serializaci贸n
toObject: () => ({
id,
firstName,
lastName,
email: normalizedEmail,
createdAt
// Observa que no incluimos el passwordHash
})
});
El proceso inverso, tomar datos planos de una base de datos o API y convertirlos nuevamente en una entidad de dominio enriquecida, es exactamente para lo que sirve tu funci贸n de f谩brica `makeUser`. Esto es deserializaci贸n.
Tipado con TypeScript o JSDoc
Si bien este patr贸n funciona perfectamente en JavaScript vainilla, agregar tipos est谩ticos con TypeScript o JSDoc lo sobrecarga. Los tipos te permiten definir formalmente la 'forma' de tu entidad, proporcionando una excelente autocompletaci贸n y comprobaciones en tiempo de compilaci贸n.
// archivo: /domain/user.ts
// Define la interfaz p煤blica de la entidad
export type User = Readonly<{
getId: () => string;
getFirstName: () => string;
// ... etc
getFullName: () => string;
}>;
// La funci贸n de f谩brica ahora devuelve el tipo User
export default function buildMakeUser(...) {
return function makeUser(...): User {
// ... implementaci贸n
}
}
Los Beneficios Generales del Patr贸n de Entidad de M贸dulo
Al adoptar este patr贸n, obtienes una multitud de beneficios que se acumulan a medida que crece tu aplicaci贸n:
- 脷nica Fuente de Verdad: Las reglas de negocio y la validaci贸n de datos est谩n centralizadas y no son ambiguas. Un cambio en una regla se realiza en exactamente un lugar.
- Alta Cohesi贸n, Bajo Acoplamiento: Las entidades son aut贸nomas y no dependen de sistemas externos. Esto hace que tu c贸digo base sea modular y f谩cil de refactorizar.
- M谩xima Capacidad de Prueba: Puedes escribir pruebas unitarias simples y r谩pidas para tu l贸gica de negocio m谩s cr铆tica sin simular el mundo entero.
- Experiencia del Desarrollador Mejorada: Cuando un desarrollador necesita trabajar con un `User`, tiene una API clara, predecible y auto-documentada para usar. No m谩s adivinar la forma de los objetos planos.
- Una Base para la Escalabilidad: Este patr贸n te brinda un n煤cleo estable y confiable. A medida que agregas m谩s caracter铆sticas, frameworks o componentes de UI, tu l贸gica de negocio permanece protegida y consistente.
Conclusi贸n: Construye un N煤cleo S贸lido para Tu Aplicaci贸n
En un mundo de frameworks y bibliotecas de r谩pido movimiento, es f谩cil olvidar que estas herramientas son transitorias. Cambiar谩n. Lo que perdura es la l贸gica central de tu dominio de negocio. Invertir tiempo en modelar adecuadamente este dominio no es solo un ejercicio acad茅mico; es una de las inversiones a largo plazo m谩s significativas que puedes hacer en la salud y la longevidad de tu software.
El Patr贸n de Entidad de M贸dulo de JavaScript proporciona una forma simple, poderosa y nativa de implementar estas ideas. No requiere un framework pesado o una configuraci贸n compleja. Aprovecha las caracter铆sticas fundamentales del lenguaje (m贸dulos, funciones y cierres) para ayudarte a construir un n煤cleo limpio, resiliente y comprensible para tu aplicaci贸n. Comienza con una entidad clave en tu pr贸ximo proyecto. Modela sus propiedades, valida su creaci贸n y dale comportamiento. Estar谩s dando el primer paso hacia una arquitectura de software m谩s robusta y profesional.